iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0

https://ithelp.ithome.com.tw/upload/images/20240927/2016820151tUfyy4Rs.png

今天要介紹的是 Mediator 模式,屬於 GoF 定義的行為型模式之一。

情境

在複雜的前端應用程式中,當模組或元件逐漸增加,各元件間的互動會變得更複雜,元件間複雜的通訊與互動關係會讓系統變得難以維護和擴展,尤其是需要修改或擴充新功能時,元件間的依賴關係(耦合度)會讓修改變得困難,甚至會影響系統的穩定性。
若元件間互動關係複雜,就會如下示意圖,當應用程式變大時就會變得難以管理。
https://ithelp.ithome.com.tw/upload/images/20240927/20168201KOEMeqiyId.jpg
圖 1 元件互動關係複雜示意圖(資料來源:自行繪製)

問題

如何在多個元件互動時,能避免它們之間的直接依賴關係?希望能有一個方法能降低元件間的耦合度,讓每個元件都能更容易的獨立開發、測試和維護,同時又能確保元件間的互動順利,能以一種方便、集中的方式管理整個系統的互動邏輯,以便修改和擴展。

權衡

  • 維持元件/模組獨立性,同時保有互動關係:希望每個元件都能獨立運行,不依賴其他元件的實現邏輯,以此降低元件間的耦合度,但同時元件之間又要能順利的互動和協作
  • 集中控制:希望有一個統一的地方來管理互動邏輯,避免每個元件各自管理複雜溝通邏輯,但集中管理可能會導致單一管理處故障時,影響系統運作(單點故障問題),增加系統複雜度

解決方案

Mediator 模式中文稱為中介者或調解者模式,是一種可允許物件在事件發生時,通知另一組物件的設計模式。Mediator 如何解決複雜的元件互動問題? 就是讓所有元件都透過 mediator 來溝通協調,當 A 元件更新時,只要告訴 mediator 即可,不須和其他元件直接互動。示意圖如下。
https://ithelp.ithome.com.tw/upload/images/20240927/20168201QNN7t9jEen.jpg
圖 2 Mediator 示意圖(資料來源:自行繪製)

Mediator 所提的「物件事件發生時通知另一組物件」乍聽之下有點像 Observer 模式中,若主體狀態更改、就通知觀察者的運作方式,但 Mediator 和 Observer 是不同的,Mediator 會扮演中央控制器角色,這個中央控制器物件會收到其他物件傳給它的事件通知,並管理這些事件,物件間的直接溝通被替換為透過 Mediator 進行的間接溝通。Mediator 讓我們能處理多個元件互相協作的情況,透過 Mediator 來交換訊息,而不需要物件間有直接的引用或依賴關係。而 Observer 則是讓一個物件(主體)能維護依賴者(觀察者)的資料,當主體物件改變時,它會通知所有觀察者,一個主體可以有多個觀察者,同時,一個觀察者也可以訂閱多個主體,Observer 可讓多個物件對單一物件狀態作出反應,而不需觀察者頻繁詢問主體的狀態。
而說到 Mediator 中央控制器、中介層的概念,在 Observer 模式的延伸版 Publish/Subscribe 也應用了中介層的概念,Publish/Subsrcibe 透過單一物件(事件頻道)來引流多個事件來源,也稱為事件聚合者(event aggregation),因此,要說的話應該是 Mediator 與 Publish/Subscribe 比較相似,但還是有本質上的不同。

在說明 Mediator 和事件聚合者的差異前,先來好好介紹 Mediator 到底是什麼、以及如何實作吧~
調解者的意思是「協助談判和解決衝突的中立方」,日常生活中如果和他人有民事或刑事的告訴乃論糾紛,我們可透過調解委員會來和對方協調如何處理糾紛,調解委員會扮演公正中立方來協助解決衝突(突然來的法律小教室...),是一個第三方的角色。

回到軟體工程,如果系統在元件間有太多直接關係,會需要一個元件成為溝通的中央控制點,用來解決過多直接關係、緊密耦合的狀態,調解者會公開統一介面,系統的不同元件可透過該介面互相溝通,並由調解者集中管理元件間互動,如此可達到系統解耦、提供元件可重用性的效果。生活中有許多調解者的例子,除了前面提到的調解委員會,例如機場的塔台就扮演調解者角色,飛機之間不會直接通訊,而是透過塔台來溝通各飛機起飛、降落事宜,集中化管理;又或者是活動策劃者,假設要規劃一場婚禮活動,會有多個供應商參與(如:餐飲、音樂、攝影等),而婚禮策劃者就是 Mediator,會負責與每個供應商溝通並協調工作,供應商之間不會直接溝通。
以前端應用來說,事件代理也算是一種調解者應用,當我們將事件綁定在整個文件(document)而非針對各別 DOM 元素訂閱時,則 document 就扮演了調解者,由 document 來將互動事件通知到各 DOM 元素。

Mediator 實作

Mediator 實作的核心概念就是一個物件,這個物件負責協調多個其他物件間的互動。它根據其他物件的動作或輸入,來決定何時應呼叫哪些物件。因此,我們可以這樣定義它:

// mediator 就是一個物件
const mediator = {};

接下來,我們來實作這個 mediator 物件。情境是一個用來新增員工資料的應用程式。這個應用程式中包含了幾個功能:新增員工資料的按鈕、輸入員工資訊,以及輸入員工管理者。這些功能何時被呼叫,都會由 orgChart 這個 mediator 物件來管理,而各功能之間則不會直接互相溝通。以下是程式碼:

 const orgChart = {
    employees: [],
    addNewEmployee() {
        // 取得輸入員工資訊功能的實例
        const employeeDetail = this.getEmployeeDetail();
        // 當員工詳細資料完成後,調解者決定接下來要發生的事
        employeeDetail.on('complete', employee => {
            const managerSelector = this.selectManager(employee);
            managerSelector.on('save', employee => {
                // 調解者使用這些事件執行附加操作
                employee.save();
                this.employees.push(employee);
                console.log('Employee saved:', employee);
            });
        });
    },

    getEmployeeDetail() {
        const detailContainer = document.createElement('div');
        const nameInput = document.createElement('input');
        nameInput.placeholder = 'Employee Name';
        detailContainer.appendChild(nameInput);
        const roleInput = document.createElement('input');
        roleInput.placeholder = 'Employee Role';
        detailContainer.appendChild(roleInput);
        const submitBtn = document.createElement('button');
        submitBtn.textContent = 'Submit';
        detailContainer.appendChild(submitBtn);

        const eventHandlers = {};

        submitBtn.addEventListener('click', () => {
            const employee = new Employee(nameInput.value, roleInput.value);
            if (eventHandlers['complete']) {
                eventHandlers['complete'](employee);
            }
        });

        document.getElementById('app').appendChild(detailContainer);

        return {
            on(event, callback) {
                eventHandlers[event] = callback;
            }
        };
    },

    selectManager(employee) {
        const managerContainer = document.createElement('div');
        const managerInput = document.createElement('input');
        managerInput.placeholder = 'Manager Name';
        managerContainer.appendChild(managerInput);
        const saveBtn = document.createElement('button');
        saveBtn.textContent = 'Save';
        managerContainer.appendChild(saveBtn);

        const eventHandlers = {};

        saveBtn.addEventListener('click', () => {
            employee.manager = managerInput.value;
            if (eventHandlers['save']) {
                eventHandlers['save'](employee);
            }
        });

        document.getElementById('app').appendChild(managerContainer);

        return {
            on(event, callback) {
                eventHandlers[event] = callback;
            }
        };
    }
};

完整程式碼請見連結,在這個範例中,我們將 addNewEmployee 方法綁定在「新增員工」按鈕的點擊事件中。orgChart 物件的 addNewEmployee 方法會呼叫 getEmployeeDetail 方法,並渲染相應的元件以讓使用者輸入員工資訊。當使用者完成輸入並觸發 complete 事件後,addNewEmployee 方法會進一步呼叫 selectManager 方法,然後監聽 save 事件。當所有步驟完成後,最終會將這些資料存入員工列表中。
在 orgChart 物件中,我們定義了多個功能(方法),並透過 addNewEmployee 方法將所有流程串聯起來。因此,我們可以稱 orgChart 這類型的物件為「工作流」(workflow)物件,同時,它也是一個 mediator,因為它負責處理其他物件間的工作流,並將這些工作流彙整到一個物件集中管理。

Mediator 與事件聚合者實作

延續新增員工的範例,我們將事件聚合者(Event Aggregator)的實作也加進來,來看看它如何實現同樣的功能。

首先,我們定義 EventAggregator class,這 class 會用 events 來儲存所有事件,並提供 on 方法來監聽事件,以及 trigger 方法來觸發事件。

class EventAggregator {
    constructor() {
        this.events = {};
    }

    on(event, callback) {
        this.events[event] = callback;
    }

    trigger(event, data) {
        if (this.events[event]) {
            this.events[event](data);
        }
    }
}

接著,我們將事件聚合者融入新增員工資料的應用中。與前面的實作不同之處在於,這次我們在 OrgChart 的 constructor 中,使用 eventAggregator 來監聽 employee:complete 和 manager:save 事件。
當「新增員工」按鈕被點擊時,會執行 addNewEmployee 方法,而此方法只會呼叫 getEmployeeDetail,在提交員工資訊時觸發 employee:complete 事件。根據之前在 constructor 中定義的事件監聽器,這個事件會觸發 assignManager 方法來處理接下來的邏輯。接著,在管理者資訊提交後,觸發 manager:save 事件,並根據事件監聽器的設定,執行 saveEmployee 方法。

const eventAggregator = new EventAggregator();

// mediator
class OrgChart {
    constructor(eventAggregator) {
        this.eventAggregator = eventAggregator;
        this.employees = [];
        this.eventAggregator.on('employee:complete', this.assignManager.bind(this));
        this.eventAggregator.on('manager:save', this.saveEmployee.bind(this));
    }

    addNewEmployee() {
        this.getEmployeeDetail();
    }

    getEmployeeDetail() {
        const detailContainer = document.createElement('div');
        const nameInput = document.createElement('input');
        nameInput.placeholder = 'Employee Name';
        detailContainer.appendChild(nameInput);
        const roleInput = document.createElement('input');
        roleInput.placeholder = 'Employee Role';
        detailContainer.appendChild(roleInput);
        const submitBtn = document.createElement('button');
        submitBtn.textContent = 'Submit';
        detailContainer.appendChild(submitBtn);

        submitBtn.addEventListener('click', () => {
            const employee = new Employee(nameInput.value, roleInput.value);
            this.eventAggregator.trigger('employee:complete', employee);
        });

        document.getElementById('app').appendChild(detailContainer);
    }

    assignManager(employee) {
        const managerContainer = document.createElement('div');
        const managerInput = document.createElement('input');
        managerInput.placeholder = 'Manager Name';
        managerContainer.appendChild(managerInput);
        const saveBtn = document.createElement('button');
        saveBtn.textContent = 'Save';
        managerContainer.appendChild(saveBtn);

        saveBtn.addEventListener('click', () => {
            employee.manager = managerInput.value;
            this.eventAggregator.trigger('manager:save', employee);
        });

        document.getElementById('app').appendChild(managerContainer);
    }

    saveEmployee(employee) {
        employee.save();
        this.employees.push(employee);
        console.log('Employee saved:', employee);
    }
}

完整程式碼請見連結,從這個範例可以看出,事件聚合者與前面 Mediator 的差異,事件聚合者不會在一個地方集中完成所有的流程,而是單純地負責觸發事件。當事件被觸發後,是否有其他元件在監聽這些事件,以及如何處理事件發生後的邏輯,這些都由其他元件來決定。因此,無法在單一地方看到完整的工作流程,事件聚合者不太適合作為工作流的呈現方式。

Mediator 與事件聚合者的共同點與差異處

Mediator 和事件聚合者之間有一些相似之處,但它們的意圖和使用方式有本質上的不同

  1. 事件的使用
    Mediator 和事件聚合者都涉及事件(event)的使用,但目的不同。事件聚合者使用事件是為了處理事件的監聽和觸發邏輯,因為它本質上就是一個管理事件發布和訂閱的機制。而 Mediator 使用事件的原因則是為了方便,但並不一定要使用事件。Mediator 也可以透過將自身參照傳遞給子物件,或者使用其他方式來建立帶有 callback 的協調機制,事件在 Mediator 中並非唯一的實現方式。
  2. 第三方物件的角色
    Mediator 和事件聚合者都充當第三方物件的角色,但其功能不同。事件聚合者是作為事件發布者和訂閱者間的中介,它的主要作用是充當事件傳遞的中心樞紐。而 Mediator 作為其他物件的第三方,則負責協調物件之間的溝通,並定義它們之間的工作流程。

這也引出了 Mediator 和事件聚合者之間的核心差異,即「邏輯和工作流程的實現位置」。事件聚合者僅僅是將事件從發布者傳遞給訂閱者,不涉及任何邏輯處理。它的作用僅限於將事件從多個來源轉發到多個處理程式,實際的工作流程和業務邏輯則分別由觸發事件的物件和處理事件的物件來決定。
相對地,Mediator 則負責彙整業務邏輯和工作流程,並決定何時調用物件的方法以及如何協調多個物件以實現所需的系統行為。

簡言之,兩者的主要差別在於「業務邏輯的所在位置」。

Mediator 與事件聚合者的適合情境

事件聚合者的適合情境

  • 有太多物件無法直接監聽、具有完全不相關物件時
  • 當物件有直接關係(如父子關係),可讓子視圖觸發事件,父視圖處理事件
  • 可將 jQuery on() 視為事件聚合者,將多個物件的事件監聽聚合到單一一個
    • 如:如果有 200 個 DOM 元素可觸發點擊事件,在 200 個 DOM 都綁定監聽器會降低效能,因此透過 on() 來聚合所有事件
  • 需要通訊但沒有直接關係的物件
    • 如:點擊功能表項目後,使用事件聚合者觸發 menu:click:foo 事件,讓 foo 物件處理點擊事件,以顯示對應的畫面內容

Mediator 的適合情境

  • 兩個或多個物件具有間接工作關係,且業務邏輯/工作流只是這些物件的互動時,可透過 mediator 解耦物件,明確定義工作流

應用案例

Express.js 是一個 web 應用程式伺服器框架,可在使用者存取路由(route)時增加 callback 來加入額外處理,而這個額外處理就屬於 mediator 的應用。
例如,如果想在根路由新增自定義 header,可在 callback 中介軟體增加這段邏輯:

const app = require('express')();

app.use('/', (req, res, next) => {
    req.header['my-header'] = 1234;
    next();
    // next() 會呼叫請求-回應循環中的下一個 callback,讓請求繼續傳遞下去
})

且我們能透過一或多個中介軟體來追蹤或修改請求,一路到回應為止。

const app = require('express')();
const html = require('./data');

app.use(
    '/', 
    (req, res, next) => {
        req.header['my-header'] = 1234;
        next();
    },
    (req, res, next) => {
        console.log(`Request has my header: ${!!req.headers['my-header']}`)
        next();
    },
);

app.get('/', (req, res) => {
    res.set('Content-Type', 'text/html');
    res.send(Buffer.from(html));
});

// 監聽 8000 port 的請求
app.listen(8000, function() {
    console.log('Server is running on 8000');
}); 

優點

以 Mediator 作為解決方案優點如下:

  • 降低耦合:元件不再直接依賴,而是透過 mediator 溝通協調,提高模組的獨立性與可測試性
  • 邏輯集中控制:元件間的互動、工作邏輯都集中在 mediator 中,方便維護和擴展

缺點

以 Mediator 作為解決方案缺點如下:

  • 單點故障:mediator 成為系統中心控制點,如果 mediator 發生問題,整個系統的互動邏輯可能會崩潰出錯
  • 複雜性高:當系統逐漸擴展,mediator 要負責的邏輯可能會變得過於複雜,難以管理

其他補充:Mediator 與 Facade

Mediator 和 Facade 雖然看起來都是將各種功能封裝在一個物件內,但兩者的本質和目的其實是不同的。

相似之處在於,它們都對現有模組進行了抽象化。然而,Facade 的目的是為整個系統提供一個更簡單、更直接的互動介面,避免暴露內部的複雜子系統。Facade 只是將這些子系統的功能集合起來,而不會增加額外的功能。外部模組只能單向呼叫 Facade 的功能,而無法感知或直接操作 Facade 背後的子系統。這種單向的關係代表 Facade 只是提供了一個簡化介面,外部模組只能向 Facade 發送請求,無法直接接觸或了解內部的細節。
Facade 關係示意:

外部模組 ---> Facade ---> (內部系統)

而 Mediator 的目的是集中管理模組間的通訊。模組間的互動不再是彼此直接通訊,而是通過 Mediator 來進行雙向通訊。這代表外部模組和 Mediator 間可以互相傳遞訊息和請求,Mediator 可以同時接收和發送訊息。因此,這是一種雙向的關係。
Mediator 關係示意:

外部模組 <---> Mediator <---> 其他外部模組

這種單向和雙向的差異也指出這兩種模式的不同目的:Facade 是為了簡化和隱藏複雜性,而 Mediator 是為了管理和協調模組間的複雜互動。

Reference


上一篇
[Day 12] Publish/Subscribe 模式
下一篇
[Day 14] Command 模式
系列文
30天的 JavaScript 設計模式之旅 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言